五、事件传递机制(Responder Chain)

UIEvent

用户对设备的交互行为,最终会在APP中被包装为UIEvent,由对应的APP进行响应和处理,用户的交互行为到UIEvent的过程如下:

  1. 用户通过硬件产生一个交互,硬件将信息传递给系统;
  2. 系统的IOKit生成IOHIDEvent,并发送给SpringBoard;
  3. SpringBoard再通过系统端口将事件传递给对应的APP;
  4. APP主线程的RunLoop中,由UIKit注册了一个Source1,监听对应端口,接收SpringBoard传递的事件;
  5. 监听的处理函数中根据传递的事件生成UIEvent,再提交到Application的事件队列中依次处理。

UIEvent有不同的类型,主要有以下几类:

  1. touches:屏幕的触控事件;
  2. motion:陀螺仪对应的时间,晃动(motionShake)是其子类型(subtype);
  3. remoteControl:遥控器(tvOS),耳机等产生的遥控时间;
  4. presses:物理按键(物理Home键等)的按压事件。

2020-05补充:
iOS13.4之后UIEvent新增了scroll、hover和transform类型,对应设备新的交互方式。

UIRespnder

UIResponder是UIKit中的一个抽象类,它有一个nextResponder属性指向下一个UIResponder,UIKit通过UIResponder实现了职责链模式,用于处理和传递UIEvent,iOS中将UIResponder实际子类的实例称为Responder(响应者),将Responder所构成的职责链称为Responder Chain(响应链),当事件被传递给Responder,Responder可以对事件做出响应,如果Responder不做处理,则默认将事件沿Responder Chain继续传递,直至被处理或最终被忽略。

Responder Chain

UIKit中的UIResponder的实际类型为UIView、UIViewController、UIApplication和AppDelegate

  1. 子View的nextResponder是其superView;
  2. View如果是ViewConroller的根视图,则它的nextResponder是其ViewController;
  3. ViewController的nextResponder是其parentViewController;
  4. ViewController如果是Window的根ViewController, 则其nextResponder是Window;
  5. Window的nextResponder是Application;
  6. Application的nextResponder是AppDelegate;

Tip:实测touches类型以外的UIEvent在传递给Application之后,不会再继续向下传递。

UITouch

不同类型的UIEvent,Application在传递给Responder Chain时有不同的处理,touches类型以外的UIEvent会直接交给FirstResponder处理或传递,touches事件则会先交给KeyWindow,再由KeyWindow通过Hit-Test找到Hit-Test View(触碰视图),然后将touches事件以及事件的UITouch对象(可能是多个)传递给Hit-Test View。

Hit-Test

Hit-Test是一个嵌套循环调用-hitTest:withEvent:方法的遍历过程,大致流程如下:

  1. Application调用KeyWindow的-hitTest:withEvent:方法;
  2. 方法中先判断自己能否接收touches事件(userInteractionEnabled/hidden/alpha),不能就直接返回nil;
  3. 再通过-pointInside:withEvent:方法判断触摸点是否在自己区域内,不在就直接返回nil;
  4. 倒序循环遍历subViews,嵌套调用subView的-hitTest:withEvent:方法,直到找到Hit-Test View;
  5. 当2、3条件都满足,subViews中也没有找到合适的Hit-Test View时,就返回View自己;
  6. Applicaton将touches事件和以及事件的UITouch对象传递给Hit-Test View。

Tip:可以通过覆写-hitTest:withEvent:-pointInside:withEvent:方法来改变APP对touches事件的响应逻辑,如改变响应对象,改变触摸响应区域等。

touches事件传递给Hit-Test View之后,如果没有被识别为UIAction,也未被识别为手势,则会沿着响应链进行传递,链上的响应者通过覆写touches的传递方法,都有机会对事件作出响应,touches的传递有以下五个相关方法:

1
2
3
4
5
6
7
8
9
10
// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application. Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches;

Tips:

  1. 覆写touches的传递方法时,如果需要继续传递,应该调用super的对应方法,而不是直接调用nextResponder的对应方法;
  2. 单点触控时UIEvent对应一个UITouch对象,多点触控则对应多个UITouch对象;
  3. 一个touches事件传递时,方法传递的UIEvent和UITouch对象是同一个(组),最后的Responder的Ended方法调用之后才销毁。

UIAction

View在收到touches事件后,在经响应链传递前,还需要先做两个判断:

  1. 判断View是否为UIControl的子类:如果是,则对touches事件做UIAction识别,识别为UIAction则通过UIControl的Tareget-Action模式进行处理,touches事件不再经响应链传递;
  2. 倒序查找View所在的视图层级是否添加了UIGestureRecognizer:如果有添加,则对touches时间做手势识别,如果被识别为手势,这通过手势的Target-Action模式进行处理,touches事件默认不再经响应链传递。

UIControl中针对UIAction的识别有四个跟踪方法,如果想自定义跟踪识别,可以覆写相关的方法:

1
2
3
4
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)cancelTrackingWithEvent:(UIEvent *)event;

Target-Action

当UIControl将touches时间识别为UIAction之后,通过Target-Action模式交个target进行处理的过程如下:

  1. 创建UIAction,调用UIControl自己的-sendAction:to:forEvent:方法,将Action转发个UIApplication对象;
  2. UIApplication对象调用-sendAction:to:fromSender:forEvent:方法,将Action分发给指定的target。

Tip:UIControl的Action在设置Target时,可以设置为nil,这样UIAction会沿着响应链寻找Target,据此可以在运行时决定Action的Target,但除非特殊需求,不建议滥用此特性。

-------------This article is over, thank you for reading -------------